/**
 * 应用保存的主题设置（如暗色模式）
 */
function applySavedTheme() {
	/**
	 * 从本地存储中获取已保存的主题
	 */
	const savedTheme = localStorage.getItem("theme");
	// 如果之前保存的是暗色模式，则应用相应样式
	if (savedTheme === "dark") {
		// 添加点击中的样式类
		themeButton.classList.add("clicking");
		// 添加暗色模式类名以启用暗色主题样式
		document.documentElement.classList.add("dark-mode");
		// 修改按钮图标为太阳图标（表示当前为暗色模式）
		themeButton.innerHTML = '<i class="fas fa-sun"></i>';
	}
};

/**
 * 显示系统消息，并在 5 秒后自动隐藏
 *
 * @param {string} message - 需要显示的系统消息内容
 *
 * @param {string} type - 消息的类型，用于指定样式类名
 */
function showSystemMessage(message, type) {
	// 将消息添加到队列
	systemMessageQueue.push({ message, type });
	// 如果当前没有消息正在显示，则立即显示队列中的第一个消息
	if (!systemMessageTimer) displayNextSystemMessage();
};

/**
 * 显示队列中的下一条系统消息
 */
function displayNextSystemMessage() {
	// 如果队列为空，直接返回
	if (systemMessageQueue.length === 0) {
		systemMessageTimer = null;
		return;
	}
	// 获取队列中的第一个消息
	const { message, type } = systemMessageQueue.shift();
	// 设置系统状态面板的文本内容为传入的消息
	systemStatusPanel.textContent = message;
	// 设置系统状态面板的类名，包含基础类名、消息类型类名和显示类名
	systemStatusPanel.className = `system-message ${type} show`;
	// 如果消息类型为错误，创建错误日志文件
	if (type === "error") createErrorLogFile(message);
	/**
	 * 隐藏系统提示
	 */
	function hideSystemMessage() {
		// 移除显示类名，隐藏系统消息面板
		systemStatusPanel.classList.remove('show');
		// 清空定时器 ID
		systemMessageTimer = null;
		// 显示下一条消息
		displayNextSystemMessage();
	};
	// 设置一个 3 秒的定时器，3 秒后隐藏系统消息面板并清空定时器 ID
	systemMessageTimer = setTimeout(hideSystemMessage, 3000);
};

/**
 * 生成记录错误信息的日志文件
 *
 * @param {string} errorMessage - 错误消息内容
 */
async function createErrorLogFile(errorMessage) {
	/** 拆分错误消息内容 */
	const splitMessage = errorMessage.split(" | ");
	// 如果拆分后的数组长度不足 3，则填充默认值
	if (splitMessage.length < 3) {
		// 清空数组内容
		splitMessage.length = 0;
		// 填充默认值
		splitMessage.push('月华出现故障啦', errorMessage, 'https://gitee.com/TayunStarry/Lunar-Astral-Agents');
	}
	/** 生成创建待处理的日志文件的名称 */
	const fileName = toBtoaString("logs/" + getSystemTime().replace(/:/g, '_') + '-' + splitMessage[0] + ".log");
	// 发送 POST 请求创建待处理的日志文件
	await fetch('/save',
		{
			method: 'POST',
			headers: {
				'Content-Type': 'text/plain',
				'X-File-Name': fileName,
				'X-Overwrite': 'true'
			},
			body: "|> 故障描述 <|: \n" + splitMessage[1] + "\n|> 故障路径 <|: \n" + splitMessage[2]
		}
	);
}

/**
 * 清空系统提示词输入框和显示区域
 */
function clearSystemPrompt() {
	// 清空输入框内容
	systemPrompt.value = "";
	// 显示清除成功的状态提示
	showSystemMessage("系统提示词已清除", "success");
};

/**
 * 切换语音引擎模式
 *
 * @param {string} mode - 模式名称，可选值为 "system" 或 "custom"
 */
function switchSpeechEngineMode(mode) {
	// 设置当前语音引擎类型
	currentSpeechEngineType = mode;
	// 判断按钮的类型
	if (mode === "system") {
		// 启用系统语音引擎按钮样式
		systemSpeechEngineButton.classList.add("active");
		// 取消自定义语音引擎按钮样式
		customSpeechEngineButton.classList.remove("active");
		// 显示系统 TTS 面板，隐藏自定义 TTS 面板
		systemSpeechEnginePanel.style.display = "block";
		customSpeechEnginePanel.style.display = "none";
	}
	else {
		// 启用自定义语音引擎按钮样式
		customSpeechEngineButton.classList.add("active");
		// 取消系统语音引擎按钮样式
		systemSpeechEngineButton.classList.remove("active");
		// 显示自定义 TTS 面板，隐藏系统 TTS 面板
		systemSpeechEnginePanel.style.display = "none";
		customSpeechEnginePanel.style.display = "block";
	}
};

/**
 * 获取系统时间
 *
 * @param {boolean} [includeTime=true] - 是否包含时间部分
 *
 * @param {boolean} [includeSeconds=true] - 是否包含秒部分（仅当includeTime为true时有效）
 *
 * @returns {string} 格式化的时间字符串
 */
function getSystemTime(includeTime = true, includeSeconds = true) {
	/** 创建一个 Date 对象，用于获取当前时间 */
	const now = new Date();
	/** 获取年份 */
	const year = now.getFullYear();
	/** 获取月份并补零 */
	const month = String(now.getMonth() + 1).padStart(2, "0");
	/** 获取日期并补零 */
	const day = String(now.getDate()).padStart(2, "0");
	// 若不包含时间，则直接返回日期字符串
	if (!includeTime) return `${year}.${month}.${day}`;
	/** 获取小时并补零 */
	const hours = String(now.getHours()).padStart(2, "0");
	/** 获取分钟并补零 */
	const minutes = String(now.getMinutes()).padStart(2, "0");
	// 若包含秒，则添加秒部分到时间字符串
	if (includeSeconds) {
		/** 获取秒并补零 */
		const seconds = String(now.getSeconds()).padStart(2, "0");
		/** 拼接并返回包含秒的时间字符串 */
		const formattedTimeWithSeconds = `${year}.${month}.${day}-${hours}:${minutes}:${seconds}`;
		// 返回包含秒的格式化时间字符串
		return formattedTimeWithSeconds;
	}
	/** 拼接并返回不包含秒的时间字符串 */
	const formattedTime = `${year}.${month}.${day}-${hours}:${minutes}`;
	// 返回格式化后的时间字符串
	return formattedTime;
};

/**
 * 清除所有配置面板的显示状态，将所有配置面板隐藏，并移除配置面板按钮的点击样式，最后重载Live2D容器。
 */
function eraseAllConfigurePanel() {
	/**
	 * 获取文档中所有的配置面板元素
	 */
	const configurePanel = document.documentElement.querySelectorAll('.configure_panel');
	/**
	 * 获取文档中所有的配置面板按钮元素
	 */
	const configurePanelButton = document.documentElement.querySelectorAll('.power-button.live2d');
	// 检查是否存在配置面板或配置面板按钮，若不存在则直接返回，避免不必要的操作
	if (configurePanel.length === 0 || configurePanelButton.length === 0) return;
	// 遍历所有配置面板，将其显示状态设置为隐藏
	configurePanel.forEach(panel => panel.style.display = 'none');
	// 遍历所有配置面板按钮，移除按钮上的点击中的样式类，恢复按钮初始样式
	configurePanelButton.forEach(button => button.classList.remove("clicking"));
	// 调用 reloadLive2DContainer 函数，重载Live2D容器
	setTimeout(reloadLive2DContainer, 500);
};

/**
 * 移除代码中的注释并处理单引号转双引号
 *
 * @param {string} codeContent - 包含注释的代码内容
 * @returns {string} - 移除注释并处理单引号后的代码内容
 */
function removeCodeComments(codeContent) {
	/**
	 * 提取 shebang 行（如果存在），shebang 行通常以 #! 开头
	 */
	const shebang = codeContent.startsWith('#!')
		? codeContent.match(/^#!.*?\n/)[0]
		: '';
	// 若存在 shebang 行，则从代码内容中移除它
	codeContent = shebang ? codeContent.slice(shebang.length) : codeContent;
	/**
	 * 移除特定格式的注释：
	 * 1. 移除行首以 %%% 开头和结尾的多行注释
	 * 2. 移除行首以 %% 开头到行尾的单行注释
	 * 3. 移除行首以 # 开头的单行注释
	 */
	codeContent = codeContent
		.replace(/%%[^\n]*\n?/g, '')
		.replace(/%%%[\s\S]*?%%%/g, '')
		.replace(/^\s*#[^\n]*\n/gm, '');
	/**
	 * 移除常见的 JavaScript 注释：
	 * 1. 移除 // 开头的单行注释
	 * 2. 移除 /* *\/ 包裹的多行注释
	 */
	codeContent = codeContent
		.replace(/\/\/[^\n]*\n/g, "")
		.replace(/\/\*[\s\S]*?\*\//g, "");
	/**
	 * 处理单引号转双引号：
	 * 1. 将转义的单引号 \' 替换为十六进制表示 \x27
	 * 2. 将单引号包裹的字符串替换为双引号包裹
	 * 3. 将十六进制表示的单引号 \x27 恢复为转义的单引号 \'
	 */
	codeContent = codeContent
		.replace(/\\'/g, '\x27')
		.replace(/'((?:[^'\\]|\\.)*?)'/g, '"$1"')
		.replace(/\x27/g, "\\'");

	// 将提取的 shebang 行重新添加到代码内容开头并返回
	return shebang + codeContent;
};

/**
 * 延迟执行管理器类，用于管理延迟执行的任务，提供任务的调用、取消等功能。
 */
class DelayExecutionManager {
	/**
	 * 计算延迟时间，将分钟转换为毫秒。
	 *
	 * @param {number} minutes - 延迟的分钟数。
	 * @returns {number} 转换后的毫秒数。
	 */
	static calculateDelayTime(minutes) {
		return minutes * 60 * 1000;
	};
	/**
	 * 调用延迟执行函数，支持取消之前相同标识符的任务。
	 *
	 * @param {string} identifier - 用于标识延迟执行任务的唯一字符串。
	 * @param {Promise<void>} callback - 延迟时间到达后执行的异步回调函数。
	 * @param {number} delay - 延迟时间（毫秒）。
	 */
	static call(identifier, callback, delay) {
		// 若存在相同标识符的任务，则清除之前的定时器
		if (delayExecutionMap.has(identifier)) clearTimeout(delayExecutionMap.get(identifier));
		/** 设置新的定时器，在指定延迟时间后执行回调函数 */
		const timeoutId = setTimeout(
			async () => {
				// 执行传入的异步回调函数
				await callback();
				// 回调函数执行完毕，从映射中移除当前任务的标识符
				delayExecutionMap.delete(identifier);
			},
			delay
		);
		// 将新的定时器 ID 存入映射中
		delayExecutionMap.set(identifier, timeoutId);
	};
	/**
	 * 根据标识符取消对应的延迟执行任务。
	 *
	 * @param {string} identifier - 用于标识延迟执行任务的唯一字符串。
	 */
	static cancel(identifier) {
		// 若映射为空或不存在该标识符，则直接返回
		if (delayExecutionMap.size === 0 || !delayExecutionMap.has(identifier)) return;
		// 清除对应的定时器
		clearTimeout(delayExecutionMap.get(identifier));
		// 从映射中删除该标识符
		delayExecutionMap.delete(identifier);
	};
	/**
	 * 取消所有延迟执行的任务。
	 */
	static cancelAll() {
		// 若映射为空，则直接返回
		if (delayExecutionMap.size === 0) return;
		// 遍历所有标识符并调用 cancel 方法取消对应的任务
		delayExecutionMap.forEach((_, identifier) => DelayExecutionManager.cancel(identifier));
	};
};

/**
 * 将输入参数转换为 Base64 编码字符串
 *
 * 此函数会先对输入参数进行 URI 编码，然后将编码后的十六进制字符转换为对应的字符，最后进行 Base64 编码
 *
 * @param {string} params - 需要转换的输入参数
 * @returns {string} Base64 编码后的字符串
 */
function toBtoaString(params) {
	/**
	 * 对输入参数进行 URI 编码，确保特殊字符被正确处理
	 */
	const encodedParams = encodeURIComponent(params);
	/**
	 * 将 URI 编码后的十六进制字符转换为对应的字符
	 */
	const decodedParams = encodedParams.replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(parseInt(p1, 16)));
	// 对转换后的字符进行 Base64 编码并返回
	return btoa(decodedParams);
};

/**
 * 约束执行器类，用于限制指定周期内函数的调用次数。
 *
 * 当调用次数未超过最大限制时执行允许回调，超过则执行禁止回调。
 */
class ConstraintExecution {
	/**
	 * 构造函数，初始化约束执行器。
	 *
	 * @param {number} periodMinutes - 约束周期，单位为分钟。
	 *
	 * @param {number} maxCount - 周期内允许的最大调用次数。
	 *
	 * @param {Function} allowedCallback - 调用次数未超过限制时执行的回调函数。
	 *
	 * @param {Function} forbiddenCallback - 调用次数超过限制时执行的回调函数。
	 */
	constructor(periodMinutes, maxCount, allowedCallback, forbiddenCallback) {
		// 将分钟转换为毫秒，存储约束周期
		this.period = periodMinutes * 60 * 1000;
		// 存储周期内允许的最大调用次数
		this.maxCount = maxCount;
		// 存储调用次数未超过限制时执行的回调函数
		this.allowedCallback = allowedCallback;
		// 存储调用次数超过限制时执行的回调函数
		this.forbiddenCallback = forbiddenCallback;
		// 用于记录每次调用的时间戳
		this.callRecords = [];
	};
	/**
	 * 执行调用，并根据当前调用次数决定执行哪个回调函数。
	 *
	 * @param {...any} args - 传递给回调函数的参数。
	 */
	run(...args) {
		/**
		 * 获取当前时间戳
		 */
		const now = Date.now();
		// 过滤掉超出约束周期的调用记录
		this.callRecords = this.callRecords.filter(timestamp => now - timestamp < this.period);
		// 如果当前调用次数小于最大限制
		if (this.callRecords.length < this.maxCount) {
			// 记录本次调用的时间戳
			this.callRecords.push(now);
			// 执行允许回调函数，并传递参数
			this.allowedCallback(...args);
		}
		// 执行禁止回调函数，并传递参数
		else this.forbiddenCallback(...args);
	}
};

/**
 * 实现元素拖动功能的函数
 *
 * @param {HTMLElement} targetElement - 需要实现拖动功能的目标元素
 */
function dragElement(targetElement) {
	/**
	 * 记录鼠标在 X 轴方向的移动差值
	 */
	let mouseXDelta = 0;
	/**
	 * 记录鼠标在 Y 轴方向的移动差值
	 */
	let mouseYDelta = 0;
	/**
	 * 记录鼠标按下时的初始 X 坐标
	 */
	let initialMouseX = 0;
	/**
	 * 记录鼠标按下时的初始 Y 坐标
	 */
	let initialMouseY = 0;
	/**
	 * 获取标题栏元素
	 */
	const headerElement = document.getElementById(targetElement.id + "-header");
	// 如果存在标题栏，仅允许通过标题栏拖动
	if (headerElement) headerElement.onmousedown = startDrag;
	// 否则允许通过整个元素拖动
	else targetElement.onmousedown = startDrag;
	/**
	 * 开始拖动元素的处理函数
	 *
	 * @param {MouseEvent} event - 鼠标事件对象
	 */
	function startDrag(event) {
		// 阻止默认事件行为
		event.preventDefault();
		// 获取鼠标初始位置
		initialMouseX = event.clientX;
		initialMouseY = event.clientY;
		// 注册鼠标释放事件，用于停止拖动
		document.onmouseup = stopDrag;
		// 注册鼠标移动事件，用于处理拖动过程
		document.onmousemove = handleElementDrag;
	};

	/**
	 * 处理元素拖动过程的函数
	 *
	 * @param {MouseEvent} event - 鼠标事件对象
	 */
	function handleElementDrag(event) {
		// 阻止默认事件行为
		event.preventDefault();
		// 计算鼠标位置的差值
		mouseXDelta = initialMouseX - event.clientX;
		mouseYDelta = initialMouseY - event.clientY;
		// 更新鼠标初始位置
		initialMouseX = event.clientX;
		initialMouseY = event.clientY;
		/**
		 * 计算元素新的顶部位置，通过当前顶部位置减去鼠标在 Y 轴的移动差值
		 */
		let newTopPosition = targetElement.offsetTop - mouseYDelta;
		/**
		 * 计算元素新的左侧位置，通过当前左侧位置减去鼠标在 X 轴的移动差值
		 */
		let newLeftPosition = targetElement.offsetLeft - mouseXDelta;
		/**
		 * 获取当前窗口的宽度，用于后续限制元素位置在屏幕范围内
		 */
		const screenWidth = window.innerWidth;
		/**
		 * 获取当前窗口的高度，用于后续限制元素位置在屏幕范围内
		 */
		const screenHeight = window.innerHeight;
		/**
		 * 获取目标元素的宽度，用于后续限制元素位置在屏幕范围内
		 */
		const elementWidth = targetElement.offsetWidth;
		/**
		 * 获取目标元素的高度，用于后续限制元素位置在屏幕范围内
		 */
		const elementHeight = targetElement.offsetHeight;
		// 约束元素位置在屏幕范围内
		newTopPosition = Math.max(0, Math.min(newTopPosition, screenHeight - elementHeight));
		newLeftPosition = Math.max(0, Math.min(newLeftPosition, screenWidth - elementWidth));
		// 设置元素的新位置
		targetElement.style.top = newTopPosition + "px";
		targetElement.style.left = newLeftPosition + "px";
	}

	/**
	 * 停止拖动元素的处理函数
	 */
	function stopDrag() {
		// 移除鼠标释放事件处理函数
		document.onmouseup = null;
		// 移除鼠标移动事件处理函数
		document.onmousemove = null;
	}
};

/**
 * 判断传入的 URL 对象是否为 localhost 格式的地址
 *
 * @param {URL} url - 需要判断的 URL 对象
 *
 * @returns {boolean} - 如果是 localhost 格式的 URL 则返回 true，否则返回 false
 */
function isLocalhostUrl(url) {
	// 验证 URL 协议是否为 HTTP 或 HTTPS，非这两种协议的 URL 直接判定不是 localhost 格式
	if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
	// 验证主机名是否为 'localhost' 或 '127.0.0.1'，不是则判定不是 localhost 格式
	if (url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') return false;
	// 若 URL 包含端口号，验证端口号是否为有效数字，非有效数字则判定不是 localhost 格式
	if (url.port && isNaN(parseInt(url.port))) return false;
	// 若上述验证都通过，则判定为 localhost 格式的 URL
	return true;
};

/**
 * 转换URL的函数
 *
 * @param {string} url - 输入的URL字符串
 *
 * @returns {string|Promise<string>} - 转换后的URL字符串
 */
async function convertUrl(url, toImage) {
	// 若URL为系统模型，则从服务器获取链接数据
	if (url.toLowerCase() == 'system') {
		/**
		 * 检查当前URL是否为localhost格式
		 */
		const isLocalhost = isLocalhostUrl(new URL(window.location.href));
		// 如果是localhost格式，直接返回/v1
		if (isLocalhost && !toImage) return '/v1';
		/**
		 * 从服务器获取链接数据
		 */
		const response = await fetch('/read/links.json');
		// 检查响应是否成功
		if (!response.ok) return url;
		/**
		 * 将响应数据解析为 JSON 格式
		 */
		const data = await response.json();
		/**
		 * 根据描述查找对应的链接
		 */
		const link = data.find(item => item.name === 'IN_API');
		// 如果找到链接且链接不为空，则使用该链接
		if (link.url && link.url.trim() !== '') {
			// 判断是否是应用于获取图片
			if (toImage) return link.url.replace("/v1", "")
			else return link.url
		}
		// 如果未找到链接或链接为空，则返回原始URL
		else return url;
	}
	else return url
}